Skip to content

fix: apply URDF material to child meshes when loadMeshCb returns a Group#333

Open
ferrolho wants to merge 5 commits into
gkjohnson:masterfrom
ferrolho:fix/apply-urdf-material-to-group-children
Open

fix: apply URDF material to child meshes when loadMeshCb returns a Group#333
ferrolho wants to merge 5 commits into
gkjohnson:masterfrom
ferrolho:fix/apply-urdf-material-to-group-children

Conversation

@ferrolho
Copy link
Copy Markdown
Contributor

@ferrolho ferrolho commented Apr 10, 2026

Summary

When loadMeshCb returns a THREE.Group (e.g. from OBJLoader), the URDF-defined material is not applied because the current code only sets .material on direct THREE.Mesh instances. Meshes loaded without their own materials (e.g. OBJ without MTL) then keep the loader-default white material.

However, some loaders return Groups with embedded materials that should be preserved (e.g. ColladaLoader, GLTFLoader). In RViz, the URDF <material> tag acts as a fallback — it only applies when the mesh has no material of its own.

This PR updates the material assignment to traverse child meshes, but only overwrite materials that have no name (i.e. loader defaults):

if (obj instanceof THREE.Mesh) {
    obj.material = material;
} else {
    obj.traverse(child => {
        if (child instanceof THREE.Mesh && !child.material.name) {
            child.material = material;
        }
    });
}

Test plan

  • Test: URDF material applied to unnamed child materials in a Group
  • Test: named embedded materials preserved when URDF material is defined
  • All existing tests pass

@gkjohnson
Copy link
Copy Markdown
Owner

Thanks! Do you mind confirming what the behavior of the "material" tag is when loading URDF models across ROS tools? Both a Collada or OBJ, for example, can both have pre-existing materials that may have colors or textures assigned to them. Does this completely overwrite that? Or are the new colors multiplied in?

One thing I've noticed (and have been frustrated by) about the ROS ecosystem is that behavior is often under-specified or modeled as "spec-by-implementation" so I'd just like to make sure this is aligned.

@ferrolho
Copy link
Copy Markdown
Contributor Author

In RViz, the URDF <material> tag acts as a fallback, not an overwrite. In robot_link.cpp, submeshes with default placeholder materials (BaseWhite / BaseWhiteNoLighting) are replaced with the URDF material, but submeshes that already have named embedded materials (from Collada, OBJ+MTL, etc.) are preserved. The robot initializes in ORIGINAL mode, so on first load you see the mesh's own colors.

The URDF spec is under-specified and doesn't say which takes precedence. But the de facto standard (RViz) treats URDF color as a fallback for meshes that lack their own materials (e.g. STL, OBJ without MTL).

I've updated the PR to match this behavior. Instead of blindly traversing all children, the new approach only applies the URDF material to child meshes whose material has no name (i.e. a loader default):

if (obj instanceof THREE.Mesh) {
    obj.material = material;
} else {
    obj.traverse(child => {
        if (child instanceof THREE.Mesh && !child.material.name) {
            child.material = material;
        }
    });
}

This preserves embedded Collada/GLTF materials while still applying URDF colors to meshes loaded without their own materials (the original bug this PR addresses). I've added a second test to verify embedded materials are preserved.

@ferrolho
Copy link
Copy Markdown
Contributor Author

Here's a before/after with the KUKA iiwa14, which uses OBJ meshes with URDF-defined material colors (Grey, Orange):

BeforeloadMeshCb returns the OBJ Group directly, URDF materials are not applied:

Before

After — URDF materials are applied to unnamed child meshes:

After

Comment thread javascript/src/URDFLoader.js Outdated
@@ -590,6 +590,18 @@ class URDFLoader {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation. In this case lets remove this obj instanceof THREE.Mesh case and just always iterate over every mesh. I don't think there's a reason to alway replace the material if just a single object is returned like this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the if/else branch. Now it always traverses the object tree. Since traverse visits the root too, a single Mesh is still handled correctly.

a5f1879

Comment thread javascript/src/URDFLoader.js Outdated

obj.traverse(child => {

if (child instanceof THREE.Mesh && !child.material.name) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit concerned about this condition. I don't think there's any guarantee that a material is named when being loaded if it's not a "default" one. Eg a GLTF model may define materials with no name. I'm wondering if there's a better method for detecting this?

In practice what are the explicit cases where a mesh should have the materials replaced? Only if a format is loaded with NO materials? Like an STL or OBJ with no MTL file?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified to always apply the URDF material to all child meshes, which is consistent with how single-Mesh returns were already handled (unconditional overwrite). Users who need to preserve embedded materials can do so in their loadMeshCb.

7a2dac9

Copy link
Copy Markdown
Owner

@gkjohnson gkjohnson Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I didn't intend to request the change be reverted. My issue is that neither the previous material.name === '' approach nor this current one match the RViz behavior, which is why I asked about which mesh formats this applies to in practice.

File formats like glTF or COLLADA will never actually have a "default" or "unspecified" material assigned because they always specify materials in the file. It seems like this embedded URDF material is only really applicable to file formats like STL or OBJ (when no MTL is used), is that right?

Users who need to preserve embedded materials can do so in their loadMeshCb.

This isn't the case. The current implementation does not actually allow for this because it always overwrites the embedded materials with the URDF-specified ones after load.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants